Rubki's Cube {The Code}


Tired of boilerplate code and endless configuration files? Same here.

I built this demo specifically to showcase how straightforward the implementation of " The Rubik's Cube"  can be. No hidden dependencies, no complex setup—just clean code that does exactly what it says on the tin.

It’s genuinely effortless to incorporate into your existing projects. Simplicity was the main feature here.

Grab the code:


<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Widget Rubik</title>
    <!-- NOTA: En tu blog, puedes omitir html/head/body si lo pegas en un post.
         Lo mantengo aquí solo para que la previsualización funcione. -->
</head>
<body style="background: #222; margin: 0; padding: 20px; font-family: sans-serif;">

    <!-- ================================================================================== -->
    <!-- COPIA DESDE AQUÍ PARA PEGAR EN BLOGGER -->
    <!-- ================================================================================== -->

    <!-- Fuente Externa (Segura para incluir en posts) -->
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">

    <style>
        /* ESTILOS SCOPED: Todo empieza con #rubik-widget-container para no afectar tu blog */
       
        #rubik-widget-container {
            /* Variables CSS locales */
            --rw-bg-dark: #050505;
            --rw-neon-blue: #00f3ff;
            --rw-neon-pink: #ff00ff;
            --rw-glass: rgba(255, 255, 255, 0.05);
            --rw-border: rgba(255, 255, 255, 0.1);

            position: relative;
            width: 100%;
            max-width: 800px;
            /* Mantiene el aspecto cuadrado responsive */
            aspect-ratio: 1 / 1;
            margin: 0 auto; /* Centrado en la columna del blog */
           
            background: radial-gradient(circle at 50% 50%, #1a1a2e 0%, #000000 100%);
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.5);
            border: 1px solid var(--rw-border);
            overflow: hidden;
            font-family: 'Orbitron', sans-serif;
            color: #fff;
            user-select: none;
            -webkit-user-select: none;
           
            /* Corrección para móviles en blogs */
            touch-action: none;
        }

        /* Reset interno para evitar herencias del blog */
        #rubik-widget-container * {
            box-sizing: border-box;
            line-height: 1.2;
        }

        #rubik-widget-container canvas {
            display: block;
            width: 100% !important;
            height: 100% !important;
            outline: none;
        }

        /* UI Overlay */
        #rubik-ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            padding: 20px;
            z-index: 10;
        }

        /* Header del Widget */
        #rubik-widget-container header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }

        #rubik-widget-container h1 {
            margin: 0;
            font-size: clamp(1rem, 4vw, 1.5rem);
            text-transform: uppercase;
            letter-spacing: 2px;
            text-shadow: 0 0 10px var(--rw-neon-blue);
            background: linear-gradient(90deg, var(--rw-neon-blue), #fff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            /* Fallback por si el blog tiene estilos raros de H1 */
            line-height: 1.2;
            padding: 0;
            border: none;
        }

        .rw-status-badge {
            background: var(--rw-glass);
            border: 1px solid var(--rw-neon-blue);
            padding: 5px 12px;
            border-radius: 20px;
            font-size: clamp(0.7rem, 3vw, 0.8rem);
            backdrop-filter: blur(5px);
            box-shadow: 0 0 10px rgba(0, 243, 255, 0.2);
            white-space: nowrap;
        }

        /* Controls Area */
        .rw-controls-area {
            pointer-events: auto;
            display: flex;
            flex-direction: column;
            gap: 10px;
            align-items: center;
            margin-bottom: 5px;
        }

        .rw-main-actions {
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
            justify-content: center;
        }

        #rubik-widget-container button {
            background: var(--rw-glass);
            border: 1px solid var(--rw-border);
            color: #fff;
            padding: 8px 16px;
            font-family: 'Orbitron', sans-serif;
            font-size: clamp(0.7rem, 2.5vw, 0.85rem);
            cursor: pointer;
            border-radius: 6px;
            transition: all 0.2s ease;
            text-transform: uppercase;
            backdrop-filter: blur(4px);
            margin: 0;
        }

        #rubik-widget-container button:hover {
            background: rgba(255, 255, 255, 0.15);
            border-color: var(--rw-neon-blue);
            box-shadow: 0 0 15px var(--rw-neon-blue);
            transform: translateY(-2px);
        }

        #rubik-widget-container button.rw-solve-btn {
            border-color: var(--rw-neon-pink);
        }
        #rubik-widget-container button.rw-solve-btn:hover {
            box-shadow: 0 0 15px var(--rw-neon-pink);
            border-color: var(--rw-neon-pink);
        }

        /* Virtual Keypad */
        .rw-keypad {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 6px;
            background: rgba(0, 0, 0, 0.5);
            padding: 8px;
            border-radius: 12px;
            border: 1px solid var(--rw-border);
        }

        .rw-key-btn {
            width: clamp(35px, 9vw, 45px) !important;
            height: clamp(35px, 9vw, 45px) !important;
            padding: 0 !important;
            display: flex !important;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: clamp(0.9rem, 3vw, 1.1rem) !important;
            font-weight: bold;
        }
       
        .rw-key-btn span {
            font-size: 0.45em;
            font-weight: normal;
            opacity: 0.7;
            margin-top: 1px;
            line-height: 1;
        }

        /* Instructions - Ocultas en móvil para ahorrar espacio */
        .rw-instructions {
            position: absolute;
            top: 50%;
            left: 20px;
            transform: translateY(-50%);
            font-size: 0.75rem;
            color: rgba(255, 255, 255, 0.7);
            pointer-events: none;
            display: none;
            background: rgba(0, 0, 0, 0.6);
            padding: 15px;
            border-radius: 10px;
            border-left: 2px solid var(--rw-neon-blue);
            max-width: 180px;
            text-align: left;
        }
       
        @media (min-width: 600px) {
            .rw-instructions { display: block; }
            .rw-keypad { display: none; }
        }

        .rw-key-hint {
            display: inline-block;
            background: #222;
            border: 1px solid #444;
            padding: 1px 6px;
            border-radius: 4px;
            margin-right: 5px;
            color: var(--rw-neon-blue);
            font-weight: bold;
            min-width: 15px;
            text-align: center;
        }
       
        .rw-move-desc {
            font-size: 0.7rem;
            color: #aaa;
            margin-left: 3px;
        }
    </style>

    <!-- Contenedor Principal del Widget -->
    <div id="rubik-widget-container">
        <div id="rubik-ui-layer">
            <header>
                <h1>Cube A.I.</h1>
                <div id="rw-status" class="rw-status-badge">LISTO</div>
            </header>

            <div class="rw-instructions">
                <p style="margin: 0 0 10px 0; color:white;"><strong>CONTROLES</strong></p>
                <div style="margin-bottom: 5px;"><span class="rw-key-hint">Click</span> Rotar Cam</div>
                <div style="height: 5px;"></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">W</span> Izq <span class="rw-move-desc">(Left)</span></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">E</span> Arriba <span class="rw-move-desc">(Up)</span></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">R</span> Der <span class="rw-move-desc">(Right)</span></div>
                <div style="height: 5px;"></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">S</span> Front <span class="rw-move-desc">(F)</span></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">D</span> Abajo <span class="rw-move-desc">(D)</span></div>
                <div style="margin-bottom: 3px;"><span class="rw-key-hint">F</span> Atrás <span class="rw-move-desc">(B)</span></div>
            </div>

            <div class="rw-controls-area">
                <!-- Mobile/Mouse Controls (Visual Keypad) -->
                <div class="rw-keypad">
                    <!-- Top Row -->
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('L')">W<span>L</span></button>
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('U')">E<span>U</span></button>
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('R')">R<span>R</span></button>
                    <!-- Bottom Row -->
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('F')">S<span>F</span></button>
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('D')">D<span>D</span></button>
                    <button class="rw-key-btn" onclick="RubikWidget.trigger('B')">F<span>B</span></button>
                </div>

                <div class="rw-main-actions">
                    <button id="rw-btn-shuffle">🔀 Mezclar</button>
                    <button id="rw-btn-solve" class="rw-solve-btn">🤖 IA Resolver</button>
                </div>
            </div>
        </div>
        <!-- Three.js Canvas se inyecta aquí -->
    </div>

    <!-- Cargar Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

    <script>
        // Namespace global para evitar colisiones con scripts de tu blog
        window.RubikWidget = (function() {
           
            const container = document.getElementById('rubik-widget-container');
            if(!container) return; // Seguridad por si no cargó el div

            // --- 1. Inicialización ---
            const scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x050505, 0.03);

            // Aspect ratio inicial basado en el contenedor
            const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100);
            camera.position.set(5, 5, 7);
            camera.lookAt(0, 0, 0);

            const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
            renderer.setSize(container.clientWidth, container.clientHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.shadowMap.enabled = true;
           
            // Adjuntar al contenedor del widget
            container.appendChild(renderer.domElement);

            // --- 2. Iluminación ---
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
            scene.add(ambientLight);

            const mainSpot = new THREE.SpotLight(0xffffff, 1);
            mainSpot.position.set(10, 20, 10);
            mainSpot.castShadow = true;
            scene.add(mainSpot);

            // Luces Neon
            const blueLight = new THREE.PointLight(0x00f3ff, 0.8, 20);
            blueLight.position.set(-5, 5, 5);
            scene.add(blueLight);

            const pinkLight = new THREE.PointLight(0xff00ff, 0.8, 20);
            pinkLight.position.set(5, -5, -5);
            scene.add(pinkLight);

            // --- 3. Cubo ---
            const cubes = [];
            const pivot = new THREE.Object3D();
            scene.add(pivot);

            const colors = {
                R: 0xe74c3c, L: 0xe67e22, U: 0xecf0f1,
                D: 0xf1c40f, F: 0x2ecc71, B: 0x3498db, Core: 0x111111
            };

            const geometry = new THREE.BoxGeometry(0.95, 0.95, 0.95);
           
            for(let x=-1; x<=1; x++) {
                for(let y=-1; y<=1; y++) {
                    for(let z=-1; z<=1; z++) {
                        const materials = [
                            new THREE.MeshStandardMaterial({ color: x===1 ? colors.R : colors.Core, roughness:0.1, metalness:0.1 }),
                            new THREE.MeshStandardMaterial({ color: x===-1 ? colors.L : colors.Core, roughness:0.1, metalness:0.1 }),
                            new THREE.MeshStandardMaterial({ color: y===1 ? colors.U : colors.Core, roughness:0.1, metalness:0.1 }),
                            new THREE.MeshStandardMaterial({ color: y===-1 ? colors.D : colors.Core, roughness:0.1, metalness:0.1 }),
                            new THREE.MeshStandardMaterial({ color: z===1 ? colors.F : colors.Core, roughness:0.1, metalness:0.1 }),
                            new THREE.MeshStandardMaterial({ color: z===-1 ? colors.B : colors.Core, roughness:0.1, metalness:0.1 }),
                        ];
                        const cube = new THREE.Mesh(geometry, materials);
                        cube.position.set(x, y, z);
                       
                        const edges = new THREE.EdgesGeometry(geometry);
                        const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({color: 0x333333}));
                        cube.add(line);
                       
                        cubes.push(cube);
                        scene.add(cube);
                    }
                }
            }

            // --- 4. Lógica Juego ---
            let isAnimating = false;
            let moveHistory = [];
            const statusEl = document.getElementById('rw-status');

            const MOVES = {
                'U': { axis: 'y', layer: 1, dir: -1 },
                'D': { axis: 'y', layer: -1, dir: 1 },
                'L': { axis: 'x', layer: -1, dir: 1 },
                'R': { axis: 'x', layer: 1, dir: -1 },
                'F': { axis: 'z', layer: 1, dir: -1 },
                'B': { axis: 'z', layer: -1, dir: 1 },
            };

            // Mapeo W,E,R,S,D,F
            const KEY_MAPPING = {
                'W': 'L', 'E': 'U', 'R': 'R',
                'S': 'F', 'D': 'D', 'F': 'B'
            };

            function getKeyByValue(axis, layer, dir) {
                for (let k in MOVES) {
                    const m = MOVES[k];
                    if (m.axis === axis && m.layer === layer && m.dir === dir) return k;
                }
                return '?';
            }

            function rotate(axis, layer, direction, speed = 300, record = true) {
                if (isAnimating) return Promise.reject("Busy");
                isAnimating = true;

                return new Promise((resolve) => {
                    const activeCubes = cubes.filter(c => Math.abs(c.position[axis] - layer) < 0.1);

                    pivot.rotation.set(0,0,0);
                    pivot.position.set(0,0,0);
                    activeCubes.forEach(c => {
                        scene.remove(c);
                        pivot.add(c);
                    });
                    scene.add(pivot);

                    const targetRot = (Math.PI / 2) * direction;
                    const startTime = Date.now();

                    function animateRotation() {
                        const now = Date.now();
                        const progress = Math.min((now - startTime) / speed, 1);
                        const ease = 1 - Math.pow(1 - progress, 4);
                        pivot.rotation[axis] = targetRot * ease;
                        if (progress < 1) requestAnimationFrame(animateRotation);
                        else finishRotation();
                    }

                    function finishRotation() {
                        pivot.rotation[axis] = targetRot;
                        pivot.updateMatrixWorld();
                        activeCubes.forEach(c => {
                            c.getWorldPosition(c.position);
                            c.getWorldQuaternion(c.quaternion);
                            c.position.x = Math.round(c.position.x);
                            c.position.y = Math.round(c.position.y);
                            c.position.z = Math.round(c.position.z);
                            c.updateMatrix();
                            pivot.remove(c);
                            scene.add(c);
                        });
                        isAnimating = false;
                        if (record) {
                            const moveKey = getKeyByValue(axis, layer, direction);
                            moveHistory.push({ axis, layer, dir: -direction, notationKey: moveKey });
                            if(moveHistory.length > 0) statusEl.innerText = "JUGANDO";
                        }
                        resolve();
                    }
                    animateRotation();
                });
            }

            // Función pública para botones
            function triggerMove(rubikNotation) {
                if (isAnimating) return;
                const m = MOVES[rubikNotation];
                if (m) rotate(m.axis, m.layer, m.dir);
            }

            // --- 5. Inputs & Cámara ---
            let isDragging = false;
            let previousMousePosition = { x: 0, y: 0 };
            let theta = 0, phi = Math.PI / 3;
            const radius = 9;

            function updateCamera() {
                camera.position.x = radius * Math.sin(theta) * Math.cos(phi);
                camera.position.y = radius * Math.sin(phi);
                camera.position.z = radius * Math.cos(theta) * Math.cos(phi);
                camera.lookAt(0, 0, 0);
            }
            updateCamera();

            // Eventos Mouse/Touch limitados al container
            container.addEventListener('mousedown', e => {
                if(e.target.tagName === 'BUTTON') return;
                isDragging = true;
                previousMousePosition = { x: e.clientX, y: e.clientY };
            });
            window.addEventListener('mouseup', () => isDragging = false);
            window.addEventListener('mousemove', e => {
                if (!isDragging) return;
                const deltaX = e.clientX - previousMousePosition.x;
                const deltaY = e.clientY - previousMousePosition.y;
                theta -= deltaX * 0.005;
                phi += deltaY * 0.005;
                phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
                updateCamera();
                previousMousePosition = { x: e.clientX, y: e.clientY };
            });

            // Touch
            container.addEventListener('touchstart', e => {
                if(e.target.tagName === 'BUTTON') return;
                isDragging = true;
                previousMousePosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }, {passive: false});
            window.addEventListener('touchend', () => isDragging = false);
            window.addEventListener('touchmove', e => {
                if (!isDragging) return;
                // Importante: Prevenir scroll SOLO si estamos tocando el cubo
                if(e.target.closest('#rubik-widget-container')) {
                   // e.preventDefault(); // Comentado para no bloquear totalmente el scroll en móviles
                }
                const deltaX = e.touches[0].clientX - previousMousePosition.x;
                const deltaY = e.touches[0].clientY - previousMousePosition.y;
                theta -= deltaX * 0.005;
                phi += deltaY * 0.005;
                phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
                updateCamera();
                previousMousePosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }, {passive: false});

            // Teclado (Global pero con preventDefault si coincide)
            window.addEventListener('keydown', (e) => {
                const key = e.key.toUpperCase();
                if (KEY_MAPPING[key]) {
                    // Evitar scroll con las flechas si se usaran, o acciones de navegador
                    // e.preventDefault();
                    triggerMove(KEY_MAPPING[key]);
                }
            });

            // Botones UI
            document.getElementById('rw-btn-shuffle').addEventListener('click', async () => {
                if(isAnimating) return;
                statusEl.innerText = "MEZCLANDO...";
                statusEl.style.borderColor = "#ff00ff";
                moveHistory = [];
                const keys = Object.keys(MOVES);
                for(let i=0; i<20; i++) {
                    const rk = keys[Math.floor(Math.random()*keys.length)];
                    const m = MOVES[rk];
                    await rotate(m.axis, m.layer, m.dir, 100, true);
                    await new Promise(r=>setTimeout(r,20));
                }
                statusEl.innerText = "MEZCLADO";
                statusEl.style.borderColor = "#00f3ff";
            });

            document.getElementById('rw-btn-solve').addEventListener('click', async () => {
                if(isAnimating || moveHistory.length === 0) return;
                statusEl.innerText = "IA RESOLVIENDO...";
                statusEl.style.borderColor = "#ff00ff";
                const path = [...moveHistory].reverse();
                moveHistory = [];
                for(const m of path) {
                    await rotate(m.axis, m.layer, m.dir, 200, false);
                    await new Promise(r=>setTimeout(r,50));
                }
                statusEl.innerText = "RESUELTO";
                statusEl.style.borderColor = "#2ecc71";
            });

            // Loop
            function animate() {
                requestAnimationFrame(animate);
                renderer.render(scene, camera);
            }
            animate();

            // Resize observando el contenedor
            const resizeObserver = new ResizeObserver(() => {
                const w = container.clientWidth;
                const h = container.clientHeight;
                camera.aspect = w / h;
                camera.updateProjectionMatrix();
                renderer.setSize(w, h);
            });
            resizeObserver.observe(container);

            return { trigger: triggerMove }; // Exponer para el HTML onclick
        })();
    </script>
   
    <!-- ================================================================================== -->
    <!-- FIN DEL CÓDIGO PARA BLOGGER -->
    <!-- ================================================================================== -->

</body>
</html>